/*
 * Created on April 3, 2005
 *
 * Copyright (c) Patrick Armstrong 2002-2007. All Rights Reserved
 */
package org.dime.mine.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;

import org.dime.mine.game.BasicBoard;
import org.dime.mine.game.Board;
import org.dime.mine.game.BoardListener;
import org.dime.mine.game.BoardUtil;
import org.dime.mine.game.GameOutcomeListener;
import org.dime.mine.test.ConfigurableBoard;

/**
 * GraphicalBoard // TODO Add type summary
 * 
 * @author Owner
 */
public class GraphicalBoard extends JPanel implements Board
{
    private static final long serialVersionUID = 0L;

    private final BasicBoard board;
    private Button[][] buttonBoard;

    public GraphicalBoard(int[][] boardLayout)
    {
        this.board = new ConfigurableBoard(boardLayout);
        createUI();
    }

    public GraphicalBoard(int rows, int columns, int mines)
    {
        this.board = new BasicBoard(rows, columns, mines);
        createUI();
    }

    public void addBoardListener(BoardListener l)
    {
        board.addBoardListener(l);
    }

    public void removeBoardListener(BoardListener l)
    {
        board.removeBoardListener(l);
    }

    public void addGameListener(GameOutcomeListener l)
    {
        board.addGameListener(l);
    }

    public void removeGameListener(GameOutcomeListener l)
    {
        board.removeGameListener(l);
    }

    @Override
    public void addMouseListener(MouseListener l)
    {
        listenerList.add(MouseListener.class, l);
    }

    @Override
    public void removeMouseListener(MouseListener l)
    {
        listenerList.remove(MouseListener.class, l);
    }

    public int check(int row, int column)
    {
        return board.check(row, column);
    }

    public int getColumns()
    {
        return board.getColumns();
    }

    public int getMarks()
    {
        return board.getMarks();
    }

    public int getMines()
    {
        return board.getMines();
    }

    public int getRows()
    {
        return board.getRows();
    }

    public boolean isGameOver()
    {
        return board.isGameOver();
    }

    public boolean isLost()
    {
        return board.isLost();
    }

    public boolean isWon()
    {
        return board.isWon();
    }

    public void open(int row, int column)
    {
        getLocation(row, column).open();
    }

    public void openBorder(int row, int column)
    {
        acceptNeighborHoodVisitor(OpenVisitor.newInstance(), row, column);
    }

    public void setMarked(int row, int column, boolean marked)
    {
        getLocation(row, column).setMarked(marked);
    }

    public void setPressed(int row, int column, boolean pressed)
    {
        ((Button) getLocation(row, column)).setPressed(pressed);
    }

    public void setPressedBorder(int row, int column, boolean pressed)
    {
        Visitor visitor = SetPressedVisitor.newInstance(pressed);
        acceptNeighborHoodVisitor(visitor, row, column);
    }

    public Board.Location getLocation(int row, int column)
    {
        return buttonBoard[row][column];
    }

    public Button getLocationFromCoordinates(Point p)
    {
        return (Button) getComponentAt(p);
    }

    @Override
    public String toString()
    {
        return board.toString();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        return board.equals(obj);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode()
    {
        return board.hashCode();
    }

    protected boolean isValidLocation(int row, int column)
    {
        return BoardUtil.isValidLocation(board, row, column);
    }

    private void createUI()
    {
        Icons.initialize();

        int rows = board.getRows();
        int columns = board.getColumns();

        this.setLayout(new GridLayout(rows, columns, 0, 0));

        this.buttonBoard = new Button[rows][columns];
        MouseListener l = new MouseHandler();
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < columns; j++)
            {
                buttonBoard[i][j] = new Button(board.getLocation(i, j));
                // TODO buttonBoard[i][j].setToolTipText("BLAH BLAH");
                buttonBoard[i][j].addMouseListener(l);
                this.add(buttonBoard[i][j]);
            }
        }

        this.setBackground(Color.gray);
    }

    private void acceptNeighborHoodVisitor(Visitor visitor, int row, int column)
    {
        for (int i = row - 1; i <= row + 1; i++)
        {
            for (int j = column - 1; j <= column + 1; j++)
            {
                if (isValidLocation(i, j) && !(i == row && j == column))
                    visitor.visit((Button) getLocation(i, j));
            }
        }
    }

    private class MouseHandler implements MouseListener
    {
        private final long MAX_CLICK_INTERVAL = 40;

        private boolean leftPressed, rightPressed;
        private long lastReleaseTime = 0;
        private int lastReleaseButton = MouseEvent.NOBUTTON;

        public void mouseClicked(MouseEvent e)
        {
            fireMouseClicked(globalizeEvent(e));
        }

        public void mouseEntered(MouseEvent e)
        {
            fireMouseEntered(globalizeEvent(e));
        }

        public void mouseExited(MouseEvent e)
        {
            fireMouseExited(globalizeEvent(e));
        }

        public void mousePressed(MouseEvent e)
        {
            Button b = (Button) e.getSource();
            int row = b.getRow();
            int col = b.getColumn();

            if (leftPressed || rightPressed)
            {
                if (e.getButton() == MouseEvent.BUTTON1)
                    leftPressed = true;
                else if (e.getButton() == MouseEvent.BUTTON3)
                    rightPressed = true;

                setPressedBorder(row, col, true);
                setPressed(row, col, true);
            }
            else if (e.getButton() == MouseEvent.BUTTON1)
            {
                setPressed(row, col, true);
                leftPressed = true;
            }
            else if (e.getButton() == MouseEvent.BUTTON3)
            {
                setMarked(row, col, board.check(row, col) != Board.MARKED);
                rightPressed = true;
            }

            fireMousePressed(globalizeEvent(e));
        }

        public void mouseReleased(MouseEvent e)
        {
            Button b = (Button) e.getSource();
            int row = b.getRow();
            int col = b.getColumn();
            long currReleaseTime = e.getWhen();

            if (currReleaseTime - lastReleaseTime <= MAX_CLICK_INTERVAL
                    && lastReleaseButton != MouseEvent.NOBUTTON)
            {
                if (buttonBoard[row][col].isSatisfied())
                    openBorder(row, col);
                else
                    setPressedBorder(row, col, false);

                leftPressed = false;
                rightPressed = false;
            }
            else if (e.getButton() == MouseEvent.BUTTON1)
            {
                if (rightPressed)
                {
                    setPressedBorder(row, col, false);
                    setPressed(row, col, false);
                }
                else
                    open(row, col);

                leftPressed = false;
            }
            else if (e.getButton() == MouseEvent.BUTTON3)
            {
                if (leftPressed)
                    setPressedBorder(row, col, false);
                rightPressed = false;
            }

            lastReleaseTime = currReleaseTime;
            lastReleaseButton = e.getButton();

            fireMouseReleased(globalizeEvent(e));
        }

        private MouseEvent globalizeEvent(MouseEvent e)
        {
            Component source = (Component) e.getSource();
            int x = source.getX() + e.getX();
            int y = source.getY() + e.getY();

            return new MouseEvent(GraphicalBoard.this, e.getID(), e.getWhen(),
                    e.getModifiers(), x, y, e.getClickCount(), e
                            .isPopupTrigger(), e.getButton());
        }

        private void fireMouseClicked(MouseEvent e)
        {
            MouseListener[] listeners = listenerList
                    .getListeners(MouseListener.class);
            for (int i = 0; i < listeners.length; i++)
            {
                listeners[i].mouseClicked(e);
            }
        }

        private void fireMouseEntered(MouseEvent e)
        {
            MouseListener[] listeners = listenerList
                    .getListeners(MouseListener.class);
            for (int i = 0; i < listeners.length; i++)
            {
                listeners[i].mouseEntered(e);
            }
        }

        private void fireMouseExited(MouseEvent e)
        {
            MouseListener[] listeners = listenerList
                    .getListeners(MouseListener.class);
            for (int i = 0; i < listeners.length; i++)
            {
                listeners[i].mouseExited(e);
            }
        }

        private void fireMousePressed(MouseEvent e)
        {
            MouseListener[] listeners = listenerList
                    .getListeners(MouseListener.class);
            for (int i = 0; i < listeners.length; i++)
            {
                listeners[i].mousePressed(e);
            }
        }

        private void fireMouseReleased(MouseEvent e)
        {
            MouseListener[] listeners = listenerList
                    .getListeners(MouseListener.class);
            for (int i = 0; i < listeners.length; i++)
            {
                listeners[i].mouseReleased(e);
            }
        }
    }

    private static interface Visitor
    {
        public void visit(Button location);
    }

    private static class SetPressedVisitor implements Visitor
    {
        private static SetPressedVisitor PRESSED = new SetPressedVisitor(true);
        private static SetPressedVisitor NOT_PRESSED = new SetPressedVisitor(
                false);

        private final boolean pressed;

        private SetPressedVisitor(boolean pressed)
        {
            this.pressed = pressed;
        }

        public void visit(Button location)
        {
            location.setPressed(pressed);
        }

        public static SetPressedVisitor newInstance(boolean pressed)
        {
            return pressed ? PRESSED : NOT_PRESSED;
        }
    }

    private static class OpenVisitor implements Visitor
    {
        private static OpenVisitor VISITOR = new OpenVisitor();

        private OpenVisitor()
        {}

        public void visit(Button location)
        {
            if (location.getValue() == Board.NOT_OPEN)
                location.open();
        }

        public static OpenVisitor newInstance()
        {
            return VISITOR;
        }
    }

    public class Button extends JLabel implements Board.Location
    {
        private static final long serialVersionUID = 0;

        private final Icon pressedIcon = Icons.getIcon(Icons.PRESSED);
        private final Icon normalIcon = Icons.getIcon(Icons.NONE);

        private final Color background = new Color(192, 192, 192);

        private final Board.Location location;

        public Button(Board.Location location)
        {
            this.setPreferredSize(new Dimension(16, 16));
            this.location = location;
            this.setOpaque(true);
            this.setIcon(normalIcon);
            this.setBackground(background);
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.dime.mine.game.Board.Location#getBoard()
         */
        @Override
        public Board getBoard()
        {
            return location.getBoard();
        }

        public int getRow()
        {
            return location.getRow();
        }

        public int getColumn()
        {
            return location.getColumn();
        }

        public int getValue()
        {
            return location.getValue();
        }

        public boolean isMarked()
        {
            return location.isMarked();
        }

        public boolean isOpen()
        {
            return location.isOpen();
        }

        public boolean isSatisfied()
        {
            return location.isSatisfied();
        }

        public void open()
        {
            if (!isOpen() && !isMarked() && !board.isGameOver())
            {
                location.open();
                setIcon(Icons.getIcon(location.getValue()));

                if (location.getValue() == 0)
                    openBorder(getRow(), getColumn());
            }
        }

        public void setMarked(boolean marked)
        {
            if (!isOpen() && !board.isGameOver())
            {
                location.setMarked(marked);
                setIcon(Icons.getIcon(location.getValue()));
            }
        }

        public void setPressed(boolean pressed)
        {
            if (!isOpen() && !isMarked() && !board.isGameOver())
            {
                if (pressed)
                    setIcon(pressedIcon);
                else
                    setIcon(Icons.getIcon(location.getValue()));
            }
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj)
        {
            return location.equals(obj);
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode()
        {
            return location.hashCode();
        }
    }
}
